用PyQt4+Python写一个简单的EPub阅读器(2/3)

上一篇,这一篇我们写GUI。

上一篇提出了图书仓库的概念,更具体的想法是:这个仓库是一个文件夹,所有打开的书都往这个文件夹中复制一份,同时,我们在仓库中有一个library.json,存放书籍清单,每次打开一本书,也在该清单中记录一份,根据清单刷新我们的Library(dockwidget目录)

目前目录结构如下:
GUI

这里只是写GUI,所以不做过多的讲解,画GUI也真的没有什么好讲的。当然,这里画GUI用的是比较繁琐的方式,用Qt creator画出界面再用pyuic4生出py文件会比较方便一点,讲真,中文的PyQt的资料实在太少了,有空的话可以写一个中文教程(好像又给自己挖坑了)。这里我就直接贴代码了。

项目中总会有一些常量,我们把它记录在constants.py中,同时这个模块进行初始化的操作,新建必要的文件夹,数据文件。

1
2
3
4
5
6
7
8
9
10
11
12
import os

PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))
LIBRARY_DIR = os.path.join(PROJECT_DIR, 'bookdata') + os.sep

if not os.path.exists(LIBRARY_DIR):
os.mkdir(LIBRARY_DIR)

LIBRARY = os.path.join(LIBRARY_DIR, "library.json")

if not os.path.exists(LIBRARY):
open(LIBRARY, 'w').close()

由于目录结构的变化,上一篇写的books.py也有一点变化,LIBRARY_DIR可以从constants模块中导入,还要加上两行

parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

这样就能从父模块中导入constants,所以books.py就变成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import zipfile
import sys

from lxml import etree
from BeautifulSoup import BeautifulStoneSoup
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

from constants import LIBRARY_DIR

# LIBRARY_DIR = os.path.abspath('.') + os.sep

RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
NAMESPACES = {
'dc': 'http://purl.org/dc/elements/1.1/',
}


class Book(object):
u"""
需要主动调用open方法才能获得相应的属性
"""
_FILE = LIBRARY_DIR + '%s.epub'

def __init__(self, book_id=None):
if book_id:
self.open(book_id)

def fromstring(self, raw, parser=RECOVER_PARSER):
return etree.fromstring(raw, parser=parser)

def read_doc_props(self, raw):
u"""

:param raw: raw string of xml
:return:
"""
root = self.fromstring(raw)
self.title = root.xpath('//dc:title', namespaces={'dc': NAMESPACES['dc']})[0].text
self.author = root.xpath('//dc:creator', namespaces={'dc': NAMESPACES['dc']})[0].text

def open(self, book_id=None):
if book_id:
self.book_id = book_id
if not self.book_id:
raise Exception('Book id not set')

self.f = zipfile.ZipFile(self._FILE % self.book_id, 'r')
soup = BeautifulStoneSoup(self.f.read('META-INF/container.xml'))

oebps = soup.findAll('rootfile')[0]['full-path']
folder = oebps.rfind(os.sep)
self.oebps_folder = '' if folder == -1 else oebps[:folder+1] # 找到oebps的文件夹名称

oebps_content = self.f.read(oebps)
self.read_doc_props(oebps_content)

opf_bs = BeautifulStoneSoup(oebps_content)
ncx = opf_bs.findAll('item', {'id': 'ncx'})[0]
ncx = self.oebps_folder + ncx['href'] # 找到ncx的完整路径

ncx_bs = BeautifulStoneSoup(self.f.read(ncx))

self.chapters = [(nav.navlabel.text, nav.content['src']) for
nav in ncx_bs.findAll('navmap')[0].findAll('navpoint')]

if __name__ == '__main__':
book = Book('莎士比亚全集')
print book.oebps_folder

print book.title
print book.author

print str(book.chapters).decode("unicode-escape").encode("utf-8")

接下来,是bookview.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PyQt4.QtGui import (QWidget, QPushButton, QHBoxLayout, QVBoxLayout,
QListWidget, QLabel, QSplitter)
from PyQt4.QtWebKit import QWebView


class BookView(QSplitter):
def __init__(self, parent=None):
super(BookView, self).__init__(parent=parent)
self.create_layout()

def create_layout(self):
self.web_view = QWebView()
self.chapter_list = QListWidget()
self.next_button = QPushButton("Next chapter")
self.previous_button = QPushButton("Previous chapter")

hbox = QHBoxLayout()
hbox.addStretch()
hbox.addWidget(self.previous_button)
hbox.addWidget(self.next_button)

vbox = QVBoxLayout()
vbox.addWidget(QLabel("Chapters"))
vbox.addWidget(self.chapter_list)
vbox.addLayout(hbox)

widget = QWidget()
widget.setLayout(vbox)

self.addWidget(self.web_view)
self.addWidget(widget)

library.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import os
import sys

from PyQt4.QtGui import QTableWidget, QTableWidgetItem
from PyQt4.QtCore import Qt

parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

from constants import LIBRARY


def get_library():
with open(LIBRARY, 'r') as f:
try:
library = json.load(f)
except Exception, e:
print(e)
library = {'books': []}
return library


def insert_library(book):
u"""

:param book: books.py中定义的类型, 有id, 有title, 有authors
:return:
"""
lib = get_library()
book.open()
lib['books'].append({'id': book.book_id, 'title': book.title, 'author': book.author})

with open(LIBRARY, 'w') as f:
json.dump(lib, f, indent=4)


# 下面的GUI代码不应该跟逻辑代码写在一起,这里的写法不是好例子
class LibraryTableWidget(QTableWidget):

def __init__(self, book_view, parent=None):
super(LibraryTableWidget, self).__init__(parent=None)
self.book_view = book_view

self.setColumnCount(2)
self.refresh()

def refresh(self):
self.library = get_library()

self.clear()
self.setRowCount(len(self.library['books']))
self.setHorizontalHeaderLabels(['Title', 'Authors'])

for i, book in enumerate(self.library['books']):
for j, cell in enumerate((book['title'], book['author'])):
item = QTableWidgetItem(cell)
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsEnabled)
self.setItem(i, j, item)

self.resizeColumnsToContents()

def create_connections(self):
pass

def view_book(self):
book_id = self.library['books'][self.currentRow()]['id']
self.book_view.load_book(book_id)

window.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import shutil

from PyQt4.QtCore import Qt, SIGNAL, SLOT
from PyQt4.QtGui import (QMainWindow, QDockWidget, QAction, QApplication,
QMessageBox, QFileDialog)

from library import LibraryTableWidget, insert_library
from bookview import BookView
from books import Book

parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)

from constants import LIBRARY_DIR


class MainWindow(QMainWindow):

def __init__(self):
super(MainWindow, self).__init__()

self.create_layout()
self.create_actions()
self.create_menus()
self.create_connections()

def create_layout(self):
self.book = BookView(self)
self.setCentralWidget(self.book)

self.create_library_dock()

def create_library_dock(self):
if getattr(self, 'dock', None):
self.dock.show()
return

self.dock = QDockWidget("Library", self)
self.dock.setAllowedAreas(Qt.LeftDockWidgetArea|Qt.RightDockWidgetArea)
self.library = LibraryTableWidget(self.book)
self.dock.setWidget(self.library)
self.addDockWidget(Qt.LeftDockWidgetArea, self.dock)

def create_menus(self):
file_menu = self.menuBar().addMenu("&File")
help_menu = self.menuBar().addMenu("&Help")

file_menu.addAction(self.library_action)
file_menu.addAction(self.open_action)
file_menu.addSeparator()
file_menu.addAction(self.quit_action)

help_menu.addAction(self.help_action)
help_menu.addAction(self.about_action)

def create_actions(self):
self.library_action = QAction("&Library", self)
self.open_action = QAction("&Open", self)
self.quit_action = QAction("&Quit", self)

self.help_action = QAction("Help", self)
self.about_action = QAction("&About", self)


def create_connections(self):
self.connect(self.library_action, SIGNAL("triggered()"), self.create_library_dock)
self.connect(self.open_action, SIGNAL("triggered()"), self.open_book)
self.connect(self.quit_action, SIGNAL("triggered()"), QApplication.instance(),
SLOT("closeAllWindows"))
self.connect(self.about_action, SIGNAL("triggered()"), self.about)
self.connect(self.help_action, SIGNAL("triggered()"), self.help)

def about(self):
QMessageBox.about(self, "QtBooks", "An ebook reader")


def help(self):
QMessageBox.information(self, 'Help', 'Nothing yet!')

def open_book(self):
book_path = QFileDialog.getOpenFileName(self, u'打开Epub格式电子书', ".", "(*.epub)")

print u"in open_book, book_name is:" + str(book_path)
print u"in open_book, bookdata path:" + str(LIBRARY_DIR)
print os.path.dirname(str(book_path))

if os.path.dirname(str(book_path))+os.sep != str(LIBRARY_DIR):
shutil.copy(str(book_path), LIBRARY_DIR)

file_name = os.path.basename(str(book_path))
book_id = file_name.split('.epub')[0]
book = Book(book_id)
insert_library(book)
self.library.refresh()

最后是main.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding: utf-8 -*-

#!/usr/bin/env python

import sys

from PyQt4.QtGui import QApplication
from src.window import MainWindow


reload(sys)
sys.setdefaultencoding('utf8')

def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

if __name__ == '__main__':
main()

MainWindow的部分除了GUI,还加上了几个无关紧要的弹出对话框的内容,涉及到Qt的信号槽机制,这部分留到下一篇。

只加上了一段逻辑代码,可以打开epub文件,并将该文件复制到仓库中(文件系统中), 同时刷新LibraryTableWidget的内容,使得书名,作者显示出来: